feat(api): /executions list, get, tool-calls endpoints#401
Draft
aryasaatvik wants to merge 5 commits intoRhysSullivan:mainfrom
Draft
feat(api): /executions list, get, tool-calls endpoints#401aryasaatvik wants to merge 5 commits intoRhysSullivan:mainfrom
aryasaatvik wants to merge 5 commits intoRhysSullivan:mainfrom
Conversation
Adds execution history persistence to the core SDK surface, wiring
three new tables (`execution`, `execution_interaction`,
`execution_tool_call`) into `coreSchema` and exposing an
`ExecutionStore` service on `executor.executions`.
Changes:
- `core-schema.ts`: three new tables with `scope_id` / `execution_id`
/ `tool_path` / `trigger_kind` / `created_at` indexes for the runs
UI's faceting + timeline queries.
- `ids.ts`: branded `ExecutionId`, `ExecutionInteractionId`,
`ExecutionToolCallId`.
- `executions.ts`: `Execution`, `ExecutionInteraction`,
`ExecutionToolCall` Schema classes, status enums,
create/update/filter/sort/meta input types, and the
`ExecutionStore` Context.Tag.
- `execution-store.ts`: `makeExecutionStore(core)` — an
adapter-backed `ExecutionStoreService` implementation. Wraps
`typedAdapter<CoreSchema>` for CRUD, handles cursor-based
pagination, filter predicates (status, trigger, tool-path glob,
time range, code substring, hadElicitation), and builds list meta
with facets + chart buckets.
- `cursor.ts`: base64url `{ createdAt, id }` pagination cursors.
- `executor.ts`: constructs the store once per executor, exposes via
`executor.executions`.
- `executions.test.ts`: round-trip + lifecycle coverage against the
in-memory adapter (no migrations needed).
Follow-up work (future PRs in the stack):
- wire the engine to record runs + tool calls through this store,
- add `/executions` API endpoints, and
- land the runs UI.
This was referenced Apr 24, 2026
0731084 to
1ca2b9b
Compare
aryasaatvik
added a commit
to aryasaatvik/executor
that referenced
this pull request
Apr 24, 2026
… mode Lands the full \`/runs\` observability UI on top of the \`/executions\` HTTP API shipped in RhysSullivan#401. Route wired in both \`apps/cloud\` and \`apps/local\`; sidebar gets a "Runs" entry. React package: - \`api/executions.tsx\` — client hitting \`GET /executions\`, \`GET /executions/:id\`, \`GET /executions/:id/tool-calls\`. Flattens the server's nested \`{ execution, pendingInteraction }\` into a single row type so components can read \`row.id\` / \`row.createdAt\` directly. Wire types are kept as epoch-ms numbers (server serializes \`Date\` at the edge) so the UI never has to know about Schema-decoded Dates. - \`pages/runs.tsx\` — infinite-scrolling list, filter rail, timeline chart, detail drawer, keyboard-driven navigation (\`j\` live, \`r\` refresh, \`/\` filter command, \`?\` help, \`↑/↓\` row nav, \`b\` collapse rail). URL params carry the filter state so runs links are shareable. - \`components/runs/*\` — 16 components covering the shell, rows, drawer, facet rail, CLI-style filter command + parser, timeline chart with drag-to-zoom, live-mode controls, and the keyboard-help overlay. - \`hooks/use-live-mode.ts\` — captures the cutoff timestamp at activation so new rows are rendered above a divider without the list jumping. \`useEffectEvent\` isolates live-toggle from stale closures. - \`hooks/use-local-storage.ts\` — SSR-safe persistence helper, used for field visibility preferences. - \`api/provider.tsx\` — wraps \`RegistryProvider\` in a shared \`QueryClientProvider\` (React Query for the list + detail fetches). - Deps added: \`@tanstack/react-query\`, \`date-fns\`, \`@date-fns/utc\`, \`react-hotkeys-hook\`. - \`styles/globals.css\` — \`--color-success|warning|error|info\` tokens used by the status dots. Apps: - \`routes/runs.tsx\` (cloud + local) — file route bound to \`<RunsPage search={Route.useSearch()} />\` with Effect-Schema validation on each query param. - \`routeTree.gen.ts\` (cloud + local) — regenerated to register \`/runs\`. The tanstack-router vite plugin will overwrite on next dev start; kept manual for now so \`bun x tsc --noEmit\` passes in CI. - \`web/shell.tsx\` (cloud + local) — sidebar NavItem for "Runs". ## Test plan - [x] \`bun x tsc --noEmit\` in \`@executor/react\`, \`apps/local\`, \`apps/cloud\` — all clean. - [x] No new runtime tests (the list/detail flow goes through the API layer which the SDK's executions.test.ts exercises end-to-end via the in-memory adapter). - [ ] Dev-server smoke: start the daemon, visit \`/runs\`, exercise filters + live mode — to be verified by reviewer or in CI once the full stack is merged.
Wires `executor.executions` into the Effect-native engine so every
`execute()` / `executeWithPause()` / `resume()` call writes an
`execution` row and its associated tool-call + interaction rows to
whichever `DBAdapter` backs the SDK.
Engine additions:
- `ExecutionTrigger` type + new `trigger?` option on `execute` and
`executeWithPause`. Callers attribute runs ("cli", "http", "mcp",
…); the kind + optional meta blob are persisted on the row.
- A stable `crypto.randomUUID()` execution id is minted at entry and
reused as `PausedExecution.id`, so callers and the DB share the
same identifier and counts line up across pause/resume.
- `makeRecordingInvoker` wraps the `SandboxToolInvoker` passed to the
code executor; each `invoke` writes a tool-call row (running →
completed|failed with duration). Storage errors are ignored so
bookkeeping failures can never fail the tool call itself.
- `persistTerminalState` runs once on fiber success or failure and
writes final status, result/error, logs, toolCallCount, completedAt.
- Pausable path: on elicitation, the execution transitions to
`waiting_for_interaction` and a pending interaction row is created;
`resume` resolves it (or cancels it if action === "cancel") before
unblocking the fiber. A `toolCallCounters` map keeps the same Ref
across pause/resume so the final count is accurate.
- Inline path: wraps the caller-supplied `onElicitation` so every
inline elicitation gets the same pending → resolved bookkeeping.
Tests (`engine-persistence.test.ts`, 5 cases) cover:
- completed run + tool call rows
- error result → status=failed, errorText captured
- toolCallCount rolls up correctly
- trigger kind + meta persist on the row
- failed tool call records status=failed with errorText
Flows the trigger: { kind, meta } option the engine added in the
previous PR end-to-end so the runs UI can facet by attribution
surface. Also promotes recording writes from Effect.ignore to a
defect-absorbing variant so a misconfigured storage backend can't
take down an execution.
Surfaces:
- HTTP API (packages/core/api): /executions POST now declares an
x-executor-trigger optional header. Handler reads it (defaulting to
"http") and passes it as the engine's trigger option.
- MCP host (packages/hosts/mcp): explicit trigger: { kind: "mcp" } on
engine.execute (inline elicitation path) and engine.executeWithPause
(paused flow).
- CLI: stamps every /executions call from executeCode with
x-executor-trigger: cli. Covers call, search, describe, sources —
every subcommand that runs code goes through this helper.
Engine robustness:
- Introduced silent helper (Effect.catchAllCause(() => Effect.void))
and swapped every bookkeeping .pipe(Effect.ignore) over to it.
Effect.ignore only catches typed failures; a synchronous throw
inside an adapter (e.g. storage-drizzle when the schema is missing
the execution model) becomes a defect and was bypassing ignore.
With silent, misconfigured storage just means no row — the
execution itself succeeds.
Verified by the MCP stdio integration test which previously leaked
the [storage-drizzle] unknown model error into the MCP tool result
text. Now returns the expected code result.
1ca2b9b to
49e7f4a
Compare
aryasaatvik
added a commit
to aryasaatvik/executor
that referenced
this pull request
Apr 24, 2026
… mode Lands the full \`/runs\` observability UI on top of the \`/executions\` HTTP API shipped in RhysSullivan#401. Route wired in both \`apps/cloud\` and \`apps/local\`; sidebar gets a "Runs" entry. React package: - \`api/executions.tsx\` — client hitting \`GET /executions\`, \`GET /executions/:id\`, \`GET /executions/:id/tool-calls\`. Flattens the server's nested \`{ execution, pendingInteraction }\` into a single row type so components can read \`row.id\` / \`row.createdAt\` directly. Wire types are kept as epoch-ms numbers (server serializes \`Date\` at the edge) so the UI never has to know about Schema-decoded Dates. - \`pages/runs.tsx\` — infinite-scrolling list, filter rail, timeline chart, detail drawer, keyboard-driven navigation (\`j\` live, \`r\` refresh, \`/\` filter command, \`?\` help, \`↑/↓\` row nav, \`b\` collapse rail). URL params carry the filter state so runs links are shareable. - \`components/runs/*\` — 16 components covering the shell, rows, drawer, facet rail, CLI-style filter command + parser, timeline chart with drag-to-zoom, live-mode controls, and the keyboard-help overlay. - \`hooks/use-live-mode.ts\` — captures the cutoff timestamp at activation so new rows are rendered above a divider without the list jumping. \`useEffectEvent\` isolates live-toggle from stale closures. - \`hooks/use-local-storage.ts\` — SSR-safe persistence helper, used for field visibility preferences. - \`api/provider.tsx\` — wraps \`RegistryProvider\` in a shared \`QueryClientProvider\` (React Query for the list + detail fetches). - Deps added: \`@tanstack/react-query\`, \`date-fns\`, \`@date-fns/utc\`, \`react-hotkeys-hook\`. - \`styles/globals.css\` — \`--color-success|warning|error|info\` tokens used by the status dots. Apps: - \`routes/runs.tsx\` (cloud + local) — file route bound to \`<RunsPage search={Route.useSearch()} />\` with Effect-Schema validation on each query param. - \`routeTree.gen.ts\` (cloud + local) — regenerated to register \`/runs\`. The tanstack-router vite plugin will overwrite on next dev start; kept manual for now so \`bun x tsc --noEmit\` passes in CI. - \`web/shell.tsx\` (cloud + local) — sidebar NavItem for "Runs". ## Test plan - [x] \`bun x tsc --noEmit\` in \`@executor/react\`, \`apps/local\`, \`apps/cloud\` — all clean. - [x] No new runtime tests (the list/detail flow goes through the API layer which the SDK's executions.test.ts exercises end-to-end via the in-memory adapter). - [ ] Dev-server smoke: start the daemon, visit \`/runs\`, exercise filters + live mode — to be verified by reviewer or in CI once the full stack is merged.
Adds the three `execution*` tables to both app drizzle schemas (sqlite + postgres) so `executor.executions` writes actually land on disk in real deployments. Until this PR, persistence silently no-op'd because the `storage-drizzle` adapter throws on unknown models (absorbed by the engine's `silent` wrapper but no row gets written). - `apps/local/src/server/executor-schema.ts`: three sqlite tables matching the DBSchema shape from `@executor/sdk` (scope_id PK on execution, standalone PK on child rows, matching indexes for scope / status / trigger_kind / created_at / tool_path / namespace). - `apps/cloud/src/services/executor-schema.ts`: mirror in pg-core with `bigint` for epoch-ms columns and `timestamp` for Date columns. - Fresh `drizzle-kit generate` output on each app's `drizzle/` dir (local `0004_fancy_red_wolf.sql`, cloud `0006_panoramic_mother_askani.sql`). No test changes — the MCP stdio integration test already exercises this path end-to-end (runs `return 2+2` through the daemon, which now successfully records + returns "4" as expected).
Extends the existing `/executions` group with the three read endpoints the runs UI needs. Handlers delegate to `executor.executions.*` (added in RhysSullivan#396 / RhysSullivan#398) and scope each read to the innermost executor scope — same rule the engine applies when writing. **Endpoints:** - `GET /executions` — list with filter + cursor + optional meta. Query params: `limit`, `cursor`, `status` (CSV), `trigger` (CSV), `tool` (CSV of paths/globs), `from`/`to` (epoch ms), `after`, `code` (substring), `sort` (`<field>,<dir>`), `elicitation` (`"true"` / `"false"`). Meta bundles facets + timeline buckets; handler only asks for it when the request isn't paginated (no `cursor` / `after`), so cheap "first page, full facets" is the default call shape. - `GET /executions/:id` — single execution detail + `pendingInteraction`. 404 on unknown id via `ExecutionNotFoundError` (already declared on the group). - `GET /executions/:id/tool-calls` — tool-call timeline. 404 on unknown execution (guard rail so empty arrays don't mask typos). **Response shape:** every `Date` is serialized to epoch ms at the handler edge (`.getTime()`) so the wire format stays numeric. The schemas in `api.ts` mirror the SDK's row projections one-to-one modulo that transform. **CSV + enum handling:** `splitCsv`, `parseSortParam`, `parseElicitationParam` live in the handler file because they're edge concerns — the SDK takes typed arrays and enums. Invalid sort fields / directions drop back to defaults (no 400). No new tests — the handlers are thin wrappers over the SDK store, which already has round-trip + filter + meta coverage in `packages/core/sdk/src/executions.test.ts`. The CSV/enum parsers are small enough to validate by inspection.
49e7f4a to
f2fa454
Compare
aryasaatvik
added a commit
to aryasaatvik/executor
that referenced
this pull request
Apr 24, 2026
… mode Lands the full \`/runs\` observability UI on top of the \`/executions\` HTTP API shipped in RhysSullivan#401. Route wired in both \`apps/cloud\` and \`apps/local\`; sidebar gets a "Runs" entry. React package: - \`api/executions.tsx\` — client hitting \`GET /executions\`, \`GET /executions/:id\`, \`GET /executions/:id/tool-calls\`. Flattens the server's nested \`{ execution, pendingInteraction }\` into a single row type so components can read \`row.id\` / \`row.createdAt\` directly. Wire types are kept as epoch-ms numbers (server serializes \`Date\` at the edge) so the UI never has to know about Schema-decoded Dates. - \`pages/runs.tsx\` — infinite-scrolling list, filter rail, timeline chart, detail drawer, keyboard-driven navigation (\`j\` live, \`r\` refresh, \`/\` filter command, \`?\` help, \`↑/↓\` row nav, \`b\` collapse rail). URL params carry the filter state so runs links are shareable. - \`components/runs/*\` — 16 components covering the shell, rows, drawer, facet rail, CLI-style filter command + parser, timeline chart with drag-to-zoom, live-mode controls, and the keyboard-help overlay. - \`hooks/use-live-mode.ts\` — captures the cutoff timestamp at activation so new rows are rendered above a divider without the list jumping. \`useEffectEvent\` isolates live-toggle from stale closures. - \`hooks/use-local-storage.ts\` — SSR-safe persistence helper, used for field visibility preferences. - \`api/provider.tsx\` — wraps \`RegistryProvider\` in a shared \`QueryClientProvider\` (React Query for the list + detail fetches). - Deps added: \`@tanstack/react-query\`, \`date-fns\`, \`@date-fns/utc\`, \`react-hotkeys-hook\`. - \`styles/globals.css\` — \`--color-success|warning|error|info\` tokens used by the status dots. Apps: - \`routes/runs.tsx\` (cloud + local) — file route bound to \`<RunsPage search={Route.useSearch()} />\` with Effect-Schema validation on each query param. - \`routeTree.gen.ts\` (cloud + local) — regenerated to register \`/runs\`. The tanstack-router vite plugin will overwrite on next dev start; kept manual for now so \`bun x tsc --noEmit\` passes in CI. - \`web/shell.tsx\` (cloud + local) — sidebar NavItem for "Runs". ## Test plan - [x] \`bun x tsc --noEmit\` in \`@executor/react\`, \`apps/local\`, \`apps/cloud\` — all clean. - [x] No new runtime tests (the list/detail flow goes through the API layer which the SDK's executions.test.ts exercises end-to-end via the in-memory adapter). - [ ] Dev-server smoke: start the daemon, visit \`/runs\`, exercise filters + live mode — to be verified by reviewer or in CI once the full stack is merged.
This was referenced Apr 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack
Depends on #400 → #399 → #398 → #396 — merge those first. Cross-fork GitHub PRs can't use a branch on a contributor fork as a base, so these five PRs display as independent in the UI. The real dependency chain is the commit graph.
feat(sdk): ExecutionStore backed by DBAdapterfeat(execution): persist engine runs + tool callsfeat(execution): trigger propagation (CLI/HTTP/MCP)feat(apps): execution tables in drizzle schemasfeat(api): /executions list/get/tool-calls endpointsUntil the earlier four land, this diff includes their commits. After they merge, this diff shrinks to just the API group + handler additions.
Summary
Extends the existing
/executionsgroup with the three read endpoints the runs UI needs. Handlers delegate toexecutor.executions.*(added in #396 / #398) and scope each read to the innermost executor scope — same rule the engine applies when writing.Endpoints
GET /executions— list with filter + cursor + optional metaQuery params:
limit— number, clamped to 1-100 by the SDK.cursor— opaque base64url token for paginating past the first page.status— CSV ofpending | running | waiting_for_interaction | completed | failed | cancelled.trigger— CSV of trigger kinds."unknown"matches rows with null.tool— CSV of tool paths or globs (github.*).from/to— epoch-ms time bounds.after— cursor-like "rows after this" filter (used by live mode).code— case-insensitive substring search.sort—<field>,<direction>(fields:createdAt,durationMs; directions:asc,desc).elicitation—"true"/"false"to filter to runs that did or didn't elicit.Meta (facets + timeline buckets + counts) is included only when the request isn't paginated (
cursor+afterboth absent). That makes "first page, full facets" the default call shape and keeps paginated fetches cheap.GET /executions/:idSingle execution detail +
pendingInteraction. 404 viaExecutionNotFoundErroron unknown id.GET /executions/:id/tool-callsTool-call timeline. 404 on unknown execution — guard rail so empty arrays don't mask typos in the path.
Implementation notes
Dateserialized to epoch ms at the handler edge (.getTime()) so the wire format stays numeric. Schemas mirror the SDK row projections one-to-one modulo that transform.splitCsv,parseSortParam,parseElicitationParam) live in the handler file because they're edge concerns — the SDK takes typed arrays and enums. Invalid sort fields / directions drop to defaults (no 400).Test plan
bun x tsc --noEmitacross sdk / execution / api / mcp — clean.bun x vitest run— sdk 97/97, execution 15/15, api 4/4, mcp 23/23.No new handler-level tests: these are thin wrappers over the SDK store which already has round-trip + filter + meta coverage in
packages/core/sdk/src/executions.test.ts. The CSV/enum parsers are small enough to validate by inspection; happy to add tests if a reviewer wants them.